/*
 * Copyright (C) 2010 Fores Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package jp.fores.midori.client.handler;

import java.io.BufferedInputStream;
import java.io.InputStream;
import java.net.URL;
import java.util.zip.GZIPInputStream;

import jp.fores.midori.client.RPCClient;
import jp.fores.midori.core.dto.RPCInputDto;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.entity.EntityTemplate;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.params.CoreConnectionPNames;
import org.apache.http.params.CoreProtocolPNames;
import org.apache.http.params.HttpProtocolParams;

/**
 * リモートプロシージャーコール用を行うクライアントの通信処理を行うハンドラのHttpClientを使った実装クラス。<br>
 * <a href="http://hc.apache.org/index.html">Apache HttpComponents</a>の <code>{@link HttpClient}</code> クラスを使って処理を行います。<br>
 * (このクラスの全てのメソッドはスレッドセーフです。)<br>
 */
public class HttpClientRPCHandler implements RPCHandler {
	//==========================================================
	//定数

	/**
	 * ログ出力用
	 */
	private static final Log log = LogFactory.getLog(HttpClientRPCHandler.class);

	/**
	 * バッファサイズ
	 */
	private static final int BUFFER_SIZE = 8192;


	//==========================================================
	//メソッド

	//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
	//抽象メソッドの実装

	/**
	 * {@inheritDoc}
	 */
	@Override
	public Object execute(RPCClient rpcClient, URL url, RPCInputDto inputDto)
			throws Exception {
		//クライアントクラスのインスタンスを生成
		//(とりあえず今はDefaultHttpClientを使う)
		HttpClient client = new DefaultHttpClient();

		try {
			//==========================================================
			//リクエストの設定

			//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
			//リクエストメソッドに応じたクラスを使用する

			//リクエストメソッドを大文字にして取得
			String requestMethod = rpcClient.requestMethod.toUpperCase();

			//リクエスト用のクラス
			//(setEntity()ができないとサーバー側にデータを送れないので、型はHttpEntityEnclosingRequestBaseに限定する)
			final HttpEntityEnclosingRequestBase request;

			//リクエストメソッドが「POST」の場合
			if (HttpPost.METHOD_NAME.equals(requestMethod)) {
				//POST用のリクエストクラスのインスタンスを使用する
				request = new HttpPost(url.toURI());
			}
			//リクエストメソッドが「PUT」の場合
			else if (HttpPut.METHOD_NAME.equals(requestMethod)) {
				//PUT用のリクエストクラスのインスタンスを使用する
				request = new HttpPut(url.toURI());
			}
			//それ以外の場合
			else {
				//どうしようもないので、不正な引数の場合の例外を投げる
				//(メッセージには大文字に変換する前のリクエストメソッドを含める)
				throw new IllegalArgumentException("リクエストメソッドが不正です。:"
						+ rpcClient.requestMethod);
			}


			//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
			//リクエストパラメーターの設定
			//(TODO:設定方法が本当にあっているかもう少し確認する)

			//文字コードに「UTF-8」を設定
			HttpProtocolParams.setContentCharset(client.getParams(), "UTF-8");

			//接続タイムアウト時間を設定
			client.getParams().setIntParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, rpcClient.connectTimeout);

			//読み取りタイムアウト時間を設定
			client.getParams().setIntParameter(CoreConnectionPNames.SO_TIMEOUT, rpcClient.readTimeout);

			//「Expect: 100-Continue handshake」機能は使わないので無効にする
			client.getParams().setBooleanParameter(CoreProtocolPNames.USE_EXPECT_CONTINUE, false);


			//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
			//リクエストヘッダの設定

			//「User-Agent」ヘッダに自分の単純なクラス名を設定
			request.addHeader("User-Agent", this.getClass().getSimpleName());

			//「Content-Type」ヘッダを設定
			//(rpcClient本体ではなく、rpcClientの中のserializerから取得する)
			request.addHeader("Content-Type", rpcClient.serializer.getContentType());

			//リクエスト送信時にgzip圧縮を行う場合
			if (rpcClient.requestGZip) {
				//「Content-Encoding」用のヘッダを設定
				request.addHeader(rpcClient.getContentEncodingHeaderName(), "gzip");
			}

			//レスポンスでgzip圧縮を受け入れる場合
			if (rpcClient.responseGZip) {
				//「Accept-Encoding」用のヘッダを設定
				request.addHeader(rpcClient.getAcceptEncodingHeaderName(), "gzip,deflate");
			}


			//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
			//サーバーに入力データを渡す

			//入力データ用のDTOの内容を出力ストリーム経由でEntityに出力するためのクラスを使う
			//(標準で用意されているEntityでは直接出力ストリームには出力できないため)
			request.setEntity(new EntityTemplate(new InternalContentProducer(rpcClient, inputDto)));


			//==========================================================
			//サーバー呼び出しを行い、レスポンスを取得する
			HttpResponse response = client.execute(request);

			//レスポンスのEntityを取得
			HttpEntity responseEntity = response.getEntity();

			//レスポンスのEntityの入力ストリームを取得
			InputStream inputStream = responseEntity.getContent();

			try {
				//レスポンスの「Content-Encoding」用のヘッダの内容を取得
				Header contentEncoding = response.getFirstHeader(rpcClient.getContentEncodingHeaderName());

				//レスポンスの「Content-Encoding」用のヘッダに「gzip」という文字列が含まれている場合
				if ((contentEncoding != null)
						&& (contentEncoding.getValue().indexOf("gzip") != -1)) {
					//デバッグログが有効な場合
					if (log.isDebugEnabled()) {
						//デバッグログ出力
						log.debug("レスポンスがgzip圧縮されているので、展開しつつ読み込みます。");
					}

					//入力ストリームをGZIPInputStreamでラッピングする
					inputStream = new GZIPInputStream(inputStream);
				}
				//それ以外の場合
				else {
					//入力ストリームをBufferedInputStreamでラッピングする
					inputStream = new BufferedInputStream(inputStream, BUFFER_SIZE);
				}

				//レスポンスの入力ストリームを処理して、リモートプロシージャコールの戻り値を取得
				//(rpcClientの中のserializerに処理を委譲する)
				Object result = rpcClient.serializer.handleRPCResponseInputStream(inputStream);

				//デバッグログが有効な場合
				if (log.isDebugEnabled()) {
					//デバッグログ出力
					log.debug("*** 「" + url + "」に対するリモートプロシージャコールが正常終了しました ***");
				}

				//リモートプロシージャコールの戻り値を返す
				return result;
			}
			//終了処理
			finally {
				//入力ストリームを確実に閉じる
				if (inputStream != null) {
					try {
						inputStream.close();
					}
					//例外処理
					catch (Exception e) {
						//どうしようもないので、例外はもみ消す
					}
				}
			}
		}
		//終了処理
		finally {
			//コネクションを閉じる
			client.getConnectionManager().shutdown();
		}
	}

}
